package project3;

import java.net.*;
import java.util.*;
import java.io.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

public class ReaderThread extends Thread {
	private DataInputStream in;
	private Socket sock;
	private Map<Integer, ProxyServerWrapper> wrapperMap;
	
	// maps stream id to byte buffer (used to buffer relay data payload)
	private Map<Integer, List<Byte>> streamToBuffer;
	
	public ReaderThread(BlockingQueue<Cell> buffer, Socket sock) {
		super();
		this.sock = sock;
		this.streamToBuffer = new ConcurrentHashMap<Integer, List<Byte>>();
		new ConcurrentHashMap<Integer, HTTPRequestResponseWorkerThread>();
		this.wrapperMap = new ConcurrentHashMap<Integer, ProxyServerWrapper>();
		try {			
			in = new DataInputStream(sock.getInputStream());
		} catch (Exception e) {
			System.err.println("DataInputStream constructor threw an exception");
			e.printStackTrace();
		}

		this.setName("ReaderThread: " + sock.getLocalPort());
	}
	
	public void run() {
		byte[] buffer = new byte[Util.CELL_LENGTH];
		
		while(true) {
			try {
				in.readFully(buffer);
				Cell curCell = Cell.createCell(buffer, Util.readCellType(buffer));
				int curCellCircId = curCell.getCircId();
				
				if (curCell.getCellType() == Util.CellType.RELAY) {
					// if (cell cmd is Relay Connected / RelayBeginFailed / RelayExtended/ RelayExtendFailed && there is a Relay worker thread waiting)
					RelayCell curRelayCell = (RelayCell) curCell;
					System.out.println("[RThread] Get Relay Response: " + curRelayCell.getRelayCmdType());
					if (curRelayCell.getRelayCmdType() == 0x04 ||
							curRelayCell.getRelayCmdType() == 0x07 ||
							curRelayCell.getRelayCmdType() == 0x0b ||
							curRelayCell.getRelayCmdType() == 0x0c) {
						// tell SourceRouter
						Router.setResponse(curRelayCell);
					} else if (IntermediateRouter.isEndRouter(curRelayCell.getCircId())) {
						System.out.println("[RThread] In End Router");
						if (curRelayCell.getRelayCmdType() == 0x01)
							// tell IntermediateRouter to create a stream / close a stream / spawn a Create worker thread
							wrapperMap.put(curRelayCell.getStreamId(), Router.associateStreamWithWebserver(curRelayCell, sock));
						 else if (curRelayCell.getRelayCmdType() == 0x06) {
							IntermediateRouter.createHop(curRelayCell, sock);
						} else { 
//							// send data to proxy's buffer
							handleRelayDataCell(curRelayCell);
						}								
					} else {// I am neither a src router nor an end router
						System.out.println("[RThread] In the intermediate Router. ");
						if (((RelayCell) curCell).getRelayCmdType() == 0x02 || ((RelayCell) curCell).getRelayCmdType() == 0x03) {
							// Relay Data: 
							// modify the cell's circuit id, put to the correct buffer
							System.out.println("[RThread] The Relay data cell.");
							if (IntermediateRouter.shouldSendBacktoBrowser(curCellCircId, sock)) {
								System.out.println("[RThread] Data sending back to the source router.");
								
//								this.resembleData(curRelayCell, new ResembleData() {
//									
//									@Override
//									public void sendData(final byte[] byteListArr, final int curStreamId, final int curCellCircId) {
//										new Thread() {
//											
//											@Override
//											public void run() {
//												SourceRouter.sendBackToBrowser(byteListArr, curStreamId, curCellCircId);
//											}
//										}.start();
//									}
//								});
								if (((RelayCell) curCell).getRelayCmdType() == 0x03) {
									SourceRouter.closeBrowserConnection(curRelayCell.getStreamId(), curCellCircId);
								} else {
									SourceRouter.sendBackToBrowser(curRelayCell.getBody(), curRelayCell.getStreamId(), curCellCircId);
								}
							} else if (IntermediateRouter.shoudSendBack(curCellCircId ,sock)) {
								IntermediateRouter.sendDataBack(curRelayCell, curCellCircId, false);
							} else {
								IntermediateRouter.setMappedCircuitId(curCellCircId, curCell);
								IntermediateRouter.putToMappedCircuitBuffer(curCell);
							}
						} else if (((RelayCell) curCell).getRelayCmdType() == 0x06) {
							IntermediateRouter.createHop(curRelayCell, sock);
						} else {
							IntermediateRouter.sendToNextRouter(curRelayCell, sock);
						}
					}
				} else { // cell is ConnectionCell
					if (curCell.getCellType() == Util.CellType.OPEN) {
						// cast the cell to an open cell
						OpenCell curOpenCell = (OpenCell) curCell;
						
						// set the from agent id
						curOpenCell.setFromAgentId(buffer);
						
						// set the to agent id
						curOpenCell.setToAgentId(buffer);

						IntermediateRouter.respondToOpen(curOpenCell, sock);
					} else if (curCell.getCellType() == Util.CellType.CREATE)
						 // tell IntermediateRouter to populate the routing table (maybe check whether the circuit id is okay or not
						 // if enforcing incoming circuit number uniqueness) and send back Created if successful
						IntermediateRouter.respondToCreate(curCell, sock);
					else if (curCell.getCellType() == Util.CellType.DESTROY)
						// tell IntermediateRouter to erase the entry in the routing table (no response)
						IntermediateRouter.destroyCircuit(curCell.getCircId());					 
					else if (curCell.getCellType() == Util.CellType.OPENED ||
							 curCell.getCellType() == Util.CellType.OPEN_FAILED ||
							 curCell.getCellType() == Util.CellType.CREATED ||
							 curCell.getCellType() == Util.CellType.CREATE_FAILED)
						Router.setResponse(curCell);						 
				}
			} catch (IOException e) {		
				Router.clean(sock);
				break;
			} 
		}
	}
	
	public void terminate() {
	}
	
	// If this router is an end router for a stream, buffers the data
	// If all of the data for the stream has been received, spawns a worker
	// thread to block-wait for response from proxy-server
	// (this method itself is non-blocking)
	private void handleRelayDataCell (RelayCell curRelayCell) {
		int curCellCircId = curRelayCell.getCircId();
		
		if (curRelayCell.getRelayCmdType() == 0x03) {
			Router.endStreamWithWebserver(curRelayCell);
		}
		
		if (IntermediateRouter.shoudSendBack(curCellCircId ,sock)) {
			IntermediateRouter.sendDataBack(curRelayCell, curCellCircId, false);
			return;
		} 
		
		this.reassembleData(curRelayCell, new ResembleData() {
			
			@Override
			public void sendData(byte[] byteListArr, int curStreamId, int curCellCircId) {
				// Spawn the worker thread. Hand over control of data to the worker thread
				HTTPRequestResponseWorkerThread wt = new HTTPRequestResponseWorkerThread(byteListArr, curStreamId, curCellCircId, wrapperMap.get(curStreamId));		
				wt.start();
			}
		});
		
//		if (!workerThreadMap.containsKey(curRelayCell.getStreamId())) {
//			HTTPRequestResponseWorkerThread wthread = new HTTPRequestResponseWorkerThread(curRelayCell.getStreamId(), curRelayCell.getCircId());
//			wthread.start();
//			workerThreadMap.put(curRelayCell.getStreamId(), wthread);
//		}
//		ProxyServerWrapper.handleRequest(curRelayCell.getStreamId(), curRelayCell.getBody());
	}
	
	private void reassembleData(RelayCell curRelayCell, ResembleData data) {
		int curCellCircId = curRelayCell.getCircId();
		int curStreamId = curRelayCell.getStreamId();
		
		byte [] curBody = curRelayCell.getBody();
		
		System.out.println("[RThread] Begin to reassemble the data. ");
		if (!streamToBuffer.containsKey(curStreamId)) {

			// initialize new buffer
			List<Byte> byteList = new ArrayList<Byte>();
			
			// add streamId and corresponding buffer to streamToBuffer
			streamToBuffer.put(curStreamId, byteList);
		} 	
	
		// get existing buffer
		List<Byte> byteList = streamToBuffer.get(curStreamId);		
			
		// append data to buffer
		for (int i = 0; i < curBody.length; i++)
			byteList.add(curBody[i]);
			
		if (curRelayCell.getBodyLength() != (Util.CELL_LENGTH - RelayCell.BODY_OFFSET)) { 
			// Reached the last segment of data in the stream. Prepare to send the data
				
			// put contents of the buffer to a byte array
			byte [] byteListArr = new byte[byteList.size()];
			for (int i = 0; i < byteList.size(); i++) {
				byteListArr[i] = byteList.get(i);
			}
			System.out.println("[RThread] Sending data.");
			data.sendData(byteListArr, curStreamId, curCellCircId);
			
			// remove the stream's buffer from the map
			streamToBuffer.remove(curStreamId);	
		}
	}
}
